1
|
|
|
import React, { PropTypes } from 'react'; |
2
|
|
|
import BaseRouterComponent from './Base'; |
3
|
|
|
|
4
|
|
|
import shallowCompare from 'react-addons-shallow-compare'; |
5
|
|
|
|
6
|
|
|
import { parse, format } from 'url'; |
7
|
|
|
import qs from 'query-string'; |
8
|
|
|
import * as actions from '../actions'; |
9
|
|
|
import { |
10
|
|
|
__DEV__, |
11
|
|
|
LINK_MATCH_EXACT, |
12
|
|
|
LINK_MATCH_PARTIAL, |
13
|
|
|
LINK_DEFAULT_METHOD, |
14
|
|
|
LINK_CLASSNAME, |
15
|
|
|
LINK_ACTIVE_CLASSNAME |
16
|
|
|
} from '../constants'; |
17
|
|
|
|
18
|
|
|
|
19
|
|
|
const compareQueryItems = (linkQueryItem, routeQueryItem) => { |
20
|
|
|
|
21
|
|
|
linkQueryItem = [].concat(linkQueryItem); |
22
|
|
|
routeQueryItem = [].concat(routeQueryItem); |
23
|
|
|
|
24
|
|
|
return linkQueryItem.reduce((result, linkQuerySubItem) => { |
25
|
|
|
result = result && routeQueryItem.includes(linkQuerySubItem.toString()); |
26
|
|
|
return result; |
27
|
|
|
}, true); |
28
|
|
|
|
29
|
|
|
}; |
30
|
|
|
|
31
|
|
|
class Link extends BaseRouterComponent { |
32
|
|
|
|
33
|
|
|
constructor(props, context) { |
34
|
|
|
|
35
|
|
|
super(props, context); |
36
|
|
|
|
37
|
|
|
this.handleClick = this.handleClick.bind(this); |
38
|
|
|
|
39
|
|
|
this.href = this.getHref(props); |
40
|
|
|
this.state = { |
41
|
|
|
isActive: false |
42
|
|
|
}; |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
componentWillReceiveProps(newProps) { |
46
|
|
|
|
47
|
|
|
if (this.props.to !== newProps.to) { |
48
|
|
|
this.href = this.getHref(newProps); |
49
|
|
|
} |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
shouldComponentUpdate(props, state) { |
53
|
|
|
|
54
|
|
|
return shallowCompare(this, props, state); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
initiateLocationChange(e) { |
58
|
|
|
const { target } = this.props; |
59
|
|
|
|
60
|
|
|
if (!target && !this.href.protocol) { |
61
|
|
|
e.preventDefault(); |
62
|
|
|
this.locationChange(this.href); |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
handleClick(e) { |
67
|
|
|
|
68
|
|
|
const { onClick } = this.props; |
69
|
|
|
|
70
|
|
|
if (typeof onClick === 'function') { |
71
|
|
|
|
72
|
|
|
const onClickResult = onClick(e); |
73
|
|
|
|
74
|
|
|
if (typeof onClickResult === 'object' && typeof onClickResult.then === 'function') { |
75
|
|
|
e.persist(); |
76
|
|
|
return onClickResult.then(() => { |
77
|
|
|
this.initiateLocationChange(e); |
78
|
|
|
}); |
79
|
|
|
} |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
return this.initiateLocationChange(e); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
getHref(props) { |
86
|
|
|
let { to } = props; |
87
|
|
|
|
88
|
|
|
if (typeof to === 'object' && to.id) { |
89
|
|
|
to = this.router.parseRoute(to); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
if (typeof to === 'string') { |
93
|
|
|
to = parse(to); |
94
|
|
|
to.query = qs.parse(to.query); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
to.hash = typeof to.hash === 'string' && to.hash[0] !== '#' ? '#' + to.hash : to.hash; |
98
|
|
|
|
99
|
|
|
return to || false; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
handleStoreChange() { |
103
|
|
|
|
104
|
|
|
if (!this.isSubscribed) return; |
105
|
|
|
|
106
|
|
|
const { activeClass, activeMatch } = this.props; |
107
|
|
|
const { pathname, hash, query, protocol } = this.href; |
108
|
|
|
|
109
|
|
|
if (!activeClass || !activeMatch || protocol) return; // eslint-disable-line consistent-return |
110
|
|
|
|
111
|
|
|
const routerStore = this.getStatefromStore(); |
112
|
|
|
const { immutable } = this.router; |
113
|
|
|
|
114
|
|
|
let isActive = true; |
115
|
|
|
|
116
|
|
|
if (activeMatch instanceof RegExp) { |
117
|
|
|
const routePath = ( immutable ? routerStore.get('path') : routerStore.path ); |
118
|
|
|
|
119
|
|
|
return this.setState({ // eslint-disable-line consistent-return |
120
|
|
|
isActive: activeMatch.test(routePath) |
121
|
|
|
}); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
if (activeMatch === LINK_MATCH_EXACT) { |
125
|
|
|
if (hash) { |
126
|
|
|
const routeHash = ( immutable ? routerStore.get('hash') : routerStore.hash ); |
127
|
|
|
isActive = isActive && hash === routeHash; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
if (query && Object.keys(query).length) { |
131
|
|
|
const routeQuery = immutable ? routerStore.get('query').toJS() : routerStore.query; |
132
|
|
|
isActive = isActive && Object.keys(query).reduce( |
133
|
|
|
(result, item) => result && compareQueryItems(query[item], routeQuery[item]), true); |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
const routePathname = ( immutable ? routerStore.get('pathname') : routerStore.pathname ); |
138
|
|
|
|
139
|
|
|
isActive = isActive && ( |
140
|
|
|
activeMatch === LINK_MATCH_EXACT |
141
|
|
|
? pathname === routePathname |
142
|
|
|
: routePathname.indexOf(pathname) === 0 |
143
|
|
|
); |
144
|
|
|
|
145
|
|
|
if (isActive !== this.state.isActive) { |
146
|
|
|
this.setState({ |
147
|
|
|
isActive |
148
|
|
|
}); |
149
|
|
|
} |
150
|
|
|
}; |
151
|
|
|
|
152
|
|
|
locationChange(to) { |
153
|
|
|
|
154
|
|
|
const { method } = this.props; |
155
|
|
|
|
156
|
|
|
let search = to.query || to.search; |
157
|
|
|
search = typeof search === 'object' ? qs.stringify(search) : search; |
158
|
|
|
|
159
|
|
|
const payload = { |
160
|
|
|
pathname: to.pathname, |
161
|
|
|
search: search, |
162
|
|
|
hash: to.hash |
163
|
|
|
}; |
164
|
|
|
|
165
|
|
|
this.store.dispatch(actions[method](payload)); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
render() { |
169
|
|
|
|
170
|
|
|
const { children, activeClass, className, target = null } = this.props; |
171
|
|
|
const classes = this.state.isActive ? `${className} ${activeClass}` : className; |
172
|
|
|
|
173
|
|
|
const props = { |
174
|
|
|
...this.props, |
175
|
|
|
target, |
176
|
|
|
href: format(this.href), |
177
|
|
|
className: classes |
178
|
|
|
}; |
179
|
|
|
|
180
|
|
|
props.onClick = this.handleClick; |
181
|
|
|
|
182
|
|
|
return React.createElement('a', props, children); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
Link.contextTypes = { |
187
|
|
|
router: PropTypes.object, |
188
|
|
|
store: PropTypes.object |
189
|
|
|
}; |
190
|
|
|
|
191
|
|
|
Link.defaultProps = { |
192
|
|
|
to: '', |
193
|
|
|
className: LINK_CLASSNAME, |
194
|
|
|
activeClass: LINK_ACTIVE_CLASSNAME, |
195
|
|
|
method: LINK_DEFAULT_METHOD, |
196
|
|
|
activeMatch: false |
197
|
|
|
}; |
198
|
|
|
|
199
|
|
|
if (__DEV__) { |
200
|
|
|
Link.propTypes = { |
201
|
|
|
to: PropTypes.oneOfType([ |
202
|
|
|
PropTypes.string, |
203
|
|
|
PropTypes.object |
204
|
|
|
]), |
205
|
|
|
className: PropTypes.string, |
206
|
|
|
activeClass: PropTypes.string, |
207
|
|
|
onClick: PropTypes.oneOfType([ |
208
|
|
|
PropTypes.instanceOf(Function), |
209
|
|
|
PropTypes.instanceOf(Promise) |
210
|
|
|
]), |
211
|
|
|
target: PropTypes.string, |
212
|
|
|
method: PropTypes.string, |
213
|
|
|
children: PropTypes.any, |
214
|
|
|
activeMatch: (props, propName, componentName) => { |
215
|
|
|
if ( |
216
|
|
|
![false, LINK_MATCH_EXACT, LINK_MATCH_PARTIAL].includes(props[propName]) && |
217
|
|
|
!(props[propName] instanceof RegExp) |
218
|
|
|
) { |
219
|
|
|
return new Error( |
220
|
|
|
'Invalid prop `' + propName + '` supplied to' + |
221
|
|
|
' `' + componentName + '`. ' + |
222
|
|
|
`Should be one of [false, '${LINK_MATCH_EXACT}', '${LINK_MATCH_PARTIAL}'] or an instance of RegExp` |
223
|
|
|
); |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
}; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
export default Link; |
230
|
|
|
|